﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.IO;
using Tesla.Content.Loaders;
using Tesla.Core;

namespace Tesla.Content {
    /// <summary>
    /// ContentManager contains functionality to locate assets, load them, and manage their lifespans. The manager
    /// also serves as a means to cache objects to ensure they only are loaded once.
    /// </summary>
    public class ContentManager : IDisposable {
        private IResourceLocator _resourceLocator;
        private Dictionary<String, Object> _loadedAssets;
        private ResourceLoaderCollection _resourceLoaders;
        private bool _isDisposed;
        private bool _useDefault;

        /// <summary>
        /// Gets if the manager has been disposed.
        /// </summary>
        public bool IsDisposed {
            get {
                return _isDisposed;
            }
        }

        /// <summary>
        /// Gets or sets if the manager should search the RenderSystemProvider's default content
        /// as a last resort.
        /// </summary>
        public bool UseDefaultContent {
            get {
                return _useDefault;
            }
            set {
                _useDefault = value;
            }
        }

        /// <summary>
        /// Creates a new content manager with a file resource locator set to the title's root path.
        /// </summary>
        public ContentManager() {
            _resourceLocator = new FileResourceLocator();
            _resourceLoaders = new ResourceLoaderCollection(this);
            _loadedAssets = new Dictionary<String, Object>();
            _isDisposed = false;
            _useDefault = true;

            RegisterLoader(new BinaryLoader());
            RegisterLoader(new MaterialLoader());
            RegisterLoader(new MaterialCollectionLoader());
        }

        /// <summary>
        /// Creates a new content manager with the specified resource locator.
        /// </summary>
        /// <param name="locator"></param>
        public ContentManager(IResourceLocator locator) {
            _resourceLocator = locator;
            _resourceLoaders = new ResourceLoaderCollection(this);
            _loadedAssets = new Dictionary<String, Object>();
            _isDisposed = false;
            _useDefault = true;

            RegisterLoader(new BinaryLoader());
            RegisterLoader(new MaterialLoader());
            RegisterLoader(new MaterialCollectionLoader());
        }

        /// <summary>
        /// Contentmanager destructor
        /// </summary>
        ~ContentManager() {
            Dispose(false);
        }

        /// <summary>
        /// Register a loader with the content manager.
        /// </summary>
        /// <param name="loader">Loader to register</param>
        public void RegisterLoader(ResourceLoader loader) {
            _resourceLoaders.Add(loader);
        }

        /// <summary>
        /// Removes a loader with the matching extension and target type from the manager.
        /// </summary>
        /// <typeparam name="T">Loader's target type</typeparam>
        /// <param name="extension">File extension</param>
        public void RemoveLoader<T>(String extension) {
            _resourceLoaders.Remove(extension, typeof(T));

        }

        /// <summary>
        /// Removes a loader with the matching extension and target type from the manager.
        /// </summary>
        /// <param name="extension">File extension</param>
        /// <param name="targetType">Loader target type</param>
        public void RemoveLoader(String extension, Type targetType) {
            _resourceLoaders.Remove(extension, targetType);
        }

        /// <summary>
        /// Gets the loader registered with the manager with the specified extension and target type.
        /// </summary>
        /// <typeparam name="T">Loader's target type</typeparam>
        /// <param name="extension">File extension</param>
        /// <returns>Resource loader</returns>
        public ResourceLoader GetLoader<T>(String extension) {
            return _resourceLoaders[extension, typeof(T)];
        }

        /// <summary>
        /// Gets the loader registered with the manager with the specified extension and target type.
        /// </summary>
        /// <param name="extension">File extension</param>
        /// <param name="targetType">Loader's target type</param>
        /// <returns>Resource loader</returns>
        public ResourceLoader GetLoader(String extension, Type targetType) {
            return _resourceLoaders[extension, targetType];
        }

        /// <summary>
        /// Clears all registered loaders from the manager and sets the default engine content loaders.
        /// </summary>
        public void ClearToDefaultLoaders() {
            _resourceLoaders.Clear();
            RegisterLoader(new BinaryLoader());
            RegisterLoader(new MaterialLoader());
            RegisterLoader(new MaterialCollectionLoader());
        }

        /// <summary>
        /// Loads the asset. If UseDefaultContent is set to true, and if the asset cannot be found,
        /// an attempt to load it from the default content manager will be made.
        /// </summary>
        /// <typeparam name="T">Type of resource</typeparam>
        /// <param name="assetName">Asset name</param>
        /// <returns>Loaded asset, if it exists, otherwise null</returns>
        /// <exception cref="System.ObjectDisposedException">Thrown if the manager has been disposed</exception>
        /// <exception cref="System.ArgumentNullException">Thrown if the asset name is null</exception>
        /// <exception cref="System.InvalidCastException">Thrown if the asset to load does not match return type</exception>
        /// <exception cref="Tesla.Content.ResourceNotFoundException">Thrown if resource could not be located</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no suitable loader registered to the manager</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if loader parameters are not valid.</exception>
        public T Load<T>(String assetName) {
            return Load<T>(assetName, LoaderParameters.None);
        }

        /// <summary>
        /// Loads the asset. If UseDefaultContent is set to true, and if the asset cannot be found,
        /// an attempt to load it from the default content manager will be made.
        /// </summary>
        /// <typeparam name="T">Type of resource</typeparam>
        /// <param name="assetName">Asset name</param>
        /// <param name="parameters">Loader parameters</param>
        /// <returns>Loaded asset, if it exists, otherwise null</returns>
        /// <exception cref="System.ObjectDisposedException">Thrown if the manager has been disposed</exception>
        /// <exception cref="System.ArgumentNullException">Thrown if the asset name is null</exception>
        /// <exception cref="System.InvalidCastException">Thrown if the asset to load does not match return type</exception>
        /// <exception cref="Tesla.Content.ResourceNotFoundException">Thrown if resource could not be located</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no suitable loader registered to the manager</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if loader parameters are not valid.</exception>
        public T Load<T>(String assetName, LoaderParameters parameters) {
            if(_loadedAssets == null) {
                throw new ObjectDisposedException("Content manager is disposed");
            }

            if(String.IsNullOrEmpty(assetName)) {
                throw new ArgumentNullException("Asset name is not valid");
            }

            if(parameters == null) {
                parameters = LoaderParameters.None;
            }

            if(!parameters.ValidateParameters()) {
                throw new TeslaException(String.Format("Error importing resource, {0} parameters are invalid.", parameters.GetType()));
            }

            Object asset;
            String fullName = _resourceLocator.GetFullPath(assetName);
            IResource resource = null;

            //First try: get the full name and see if its been loaded, if not try to locate the resource
            if(!parameters.OverrideCache && _loadedAssets.TryGetValue(fullName, out asset)) {
                if(asset is T) {
                    return (T) asset;
                } else {
                    throw new InvalidCastException("Load error: Asset to retrieve does not match specified asset type.");
                }
            } else {
                resource =  _resourceLocator.LocateResource(assetName);
            }

            //Second try, Try the default content
            if(resource == null) {
                if(!_useDefault) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName));
                }
                try {
                    return Engine.RenderSystemProvider.DefaultContent.Load<T>(assetName, parameters);
                } catch(Exception e) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName), e.InnerException);
                }
            }

            //Find the appropiate resource loader
            Type targetType = typeof(T);
            ResourceLoader loader = _resourceLoaders[resource.Extension, targetType];
            if(loader == null) {
                throw new InvalidOperationException(String.Format("Error loading resource, no suitable loader for that {0} extension found.", resource.Extension));
            } 

            //Load the asset
            asset = loader.Load(resource, targetType, parameters);
            if(!(asset is T)) {
                throw new InvalidCastException("Load error: Asset to retrieve does not match specified asset type.");
            }

            //Cache the asset and return
            _loadedAssets[fullName] = asset;
            return (T) asset;
        }

        /// <summary>
        /// Loads the asset using an overloaded resource locator. If UseDefaultContent is set to true, and if the asset cannot be found,
        /// an attempt to load it from the default content manager will be made.
        /// </summary>
        /// <typeparam name="T">Type of resource</typeparam>
        /// <param name="assetName">Asset name</param>
        /// <param name="parameters">Loader parameters</param>
        /// <param name="locatorOverride">Resource locator to use</param>
        /// <returns>Loaded asset, if it exists, otherwise null</returns>
        /// <exception cref="System.ObjectDisposedException">Thrown if the manager has been disposed</exception>
        /// <exception cref="System.ArgumentNullException">Thrown if the asset name or locator is null</exception>
        /// <exception cref="System.InvalidCastException">Thrown if the asset to load does not match return type</exception>
        /// <exception cref="Tesla.Content.ResourceNotFoundException">Thrown if resource could not be located</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no suitable loader registered to the manager</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if loader parameters are not valid.</exception>
        public T Load<T>(String assetName, LoaderParameters parameters, IResourceLocator locatorOverride) {
            if(_loadedAssets == null) {
                throw new ObjectDisposedException("Content manager is disposed");
            }

            if(String.IsNullOrEmpty(assetName)) {
                throw new ArgumentNullException("Asset name is not valid");
            }

            IResourceLocator oldLocator = _resourceLocator;
            if(locatorOverride == null) {
                throw new ArgumentNullException("locatorOverride", "Resource locator override cannot be null");
            }
            _resourceLocator = locatorOverride;

            if(parameters == null) {
                parameters = LoaderParameters.None;
            }

            if(!parameters.ValidateParameters()) {
                throw new TeslaException(String.Format("Error importing resource, {0} parameters are invalid.", parameters.GetType()));
            }

            Object asset;
            String fullName = _resourceLocator.GetFullPath(assetName);
            IResource resource = null;

            //First try: get the full name and see if its been loaded, if not try to locate the resource
            if(!parameters.OverrideCache && _loadedAssets.TryGetValue(fullName, out asset)) {
                if(asset is T) {
                    return (T) asset;
                } else {
                    throw new InvalidCastException("Load error: Asset to retrieve does not match specified asset type.");
                }
            } else {
                resource =  _resourceLocator.LocateResource(assetName);
            }

            //Second try, Try the default content
            if(resource == null) {
                if(!_useDefault) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName));
                }
                try {
                    return Engine.RenderSystemProvider.DefaultContent.Load<T>(assetName, parameters);
                } catch(Exception e) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName), e.InnerException);
                }
            }

            //Find the appropiate resource loader
            Type targetType = typeof(T);
            ResourceLoader loader = _resourceLoaders[resource.Extension, targetType];
            if(loader == null) {
                throw new InvalidOperationException(String.Format("Error loading resource, no suitable loader for that {0} extension found.", resource.Extension));
            }

            //Load the asset
            asset = loader.Load(resource, targetType, parameters);
            if(!(asset is T)) {
                throw new InvalidCastException("Load error: Asset to retrieve does not match specified asset type.");
            }

            //Cache the asset and return
            _loadedAssets[fullName] = asset;
            _resourceLocator = oldLocator;
            return (T) asset;
        }

        /// <summary>
        /// Loads the asset relative to another asset source. If UseDefaultContent is set to true, and if the asset cannot be found,
        /// an attempt to load it from the default content manager will be made.
        /// </summary>
        /// <typeparam name="T">Type of resource</typeparam>
        /// <param name="assetName">Asset name</param>
        /// <param name="relativeResource">Relative asset source</param>
        /// <returns>Loaded asset, if it exists, otherwise null</returns>
        /// <exception cref="System.ObjectDisposedException">Thrown if the manager has been disposed</exception>
        /// <exception cref="System.ArgumentNullException">Thrown if the asset name is null</exception>
        /// <exception cref="System.InvalidCastException">Thrown if the asset to load does not match return type</exception>
        /// <exception cref="Tesla.Content.ResourceNotFoundException">Thrown if resource could not be located</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no suitable loader registered to the manager</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if loader parameters are not valid.</exception>
        public T LoadRelativeTo<T>(String assetName, IResource relativeResource) {
            return LoadRelativeTo<T>(assetName, relativeResource, LoaderParameters.None);
        }

        /// <summary>
        /// Loads the asset relative to another asset source. If UseDefaultContent is set to true, and if the asset cannot be found,
        /// an attempt to load it from the default content manager will be made.
        /// </summary>
        /// <typeparam name="T">Type of resource</typeparam>
        /// <param name="assetName">Asset name</param>
        /// <param name="relativeResource">Relative asset source</param>
        /// <param name="parameters">Loader parameters</param>
        /// <returns>Loaded asset, if it exists, otherwise null</returns>
        /// <exception cref="System.ObjectDisposedException">Thrown if the manager has been disposed</exception>
        /// <exception cref="System.ArgumentNullException">Thrown if the asset name is null</exception>
        /// <exception cref="System.InvalidCastException">Thrown if the asset to load does not match return type</exception>
        /// <exception cref="Tesla.Content.ResourceNotFoundException">Thrown if resource could not be located</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no suitable loader registered to the manager</exception>
        /// <exception cref="Tesla.Core.TeslaException">Thrown if loader parameters are not valid.</exception>
        public T LoadRelativeTo<T>(String assetName, IResource relativeResource, LoaderParameters parameters) {
            if(_loadedAssets == null) {
                throw new ObjectDisposedException("Content manager is disposed");
            }

            if(relativeResource == null) {
                throw new ArgumentNullException("Resource to use to locate relative resource cannot be null");
            }

            if(String.IsNullOrEmpty(assetName)) {
                throw new ArgumentNullException("Asset name is not valid");
            }

            if(parameters == null) {
                parameters = LoaderParameters.None;
            }

            if(!parameters.ValidateParameters()) {
                throw new TeslaException(String.Format("Error importing resource, {0} parameters are invalid.", parameters.GetType()));
            }

            Object asset;
            String fullName = _resourceLocator.GetFullPathRelativeTo(assetName, relativeResource);
            IResource resource = null;

            //Try the default content first
            if(String.IsNullOrEmpty(fullName)) {
                if(!_useDefault) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName));
                }
                try {
                    return Engine.RenderSystemProvider.DefaultContent.LoadRelativeTo<T>(assetName, relativeResource, parameters);
                } catch(Exception e) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName), e.InnerException);
                }
            }

            //Get the full name and see if its been loaded, if not try to locate the resource
            if(!parameters.OverrideCache && _loadedAssets.TryGetValue(fullName, out asset)) {
                if(asset is T) {
                    return (T) asset;
                } else {
                    throw new InvalidCastException("Load error: Asset to retrieve does not match specified asset type.");
                }
            } else {
                resource =  _resourceLocator.LocateResource(fullName);
            }

            Type targetType = typeof(T);
            //Find the appropiate resource loader
            ResourceLoader loader = _resourceLoaders[resource.Extension, targetType];
            if(loader == null) {
                throw new InvalidOperationException(String.Format("Error loading resource, no suitable loader for that {0} extension found.", resource.Extension));
            }

            //Load the asset
            asset = loader.Load(resource, targetType, parameters);
            if(!(asset is T)) {
                throw new InvalidCastException("Load error: Asset to retrieve does not match specified asset type.");
            }

            //Cache the asset and return
            _loadedAssets[fullName] = asset;
            return (T) asset;
        }

        /// <summary>
        /// Unloads all content cached by the manager. Objects that implement IDisposable
        /// will be disposed.
        /// </summary>
        /// <exception cref="System.ObjectDisposedException">Thrown if the manager has been disposed</exception>
        public void Unload() {
            if(_isDisposed) {
                throw new ObjectDisposedException("Content manager is already disposed");
            }
            try {
                foreach(KeyValuePair<String, Object> kv in _loadedAssets) {
                    IDisposable disposable = kv.Value as IDisposable;
                    if(disposable != null) {
                        disposable.Dispose();
                    }
                }
            } finally {
                _loadedAssets.Clear();
            }

        }

        /// <summary>
        /// Locates the specified asset and opens a stream to read it. If UseDefaultContent is set to true and 
        /// the resource cannot be found, an attempt to locate it from default content will be made.
        /// </summary>
        /// <param name="assetName">Asset name</param>
        /// <returns>Stream</returns>
        public Stream OpenStream(String assetName) {
            if(String.IsNullOrEmpty(assetName)) {
                throw new NullReferenceException("Asset name is not valid");
            }

            IResource resource = null;

            //First try: Try to locate the resource with the manager's locator
            resource =  _resourceLocator.LocateResource(assetName);

            //Second try, if resource is not located then try the default content
            if(resource == null) {
                if(!_useDefault) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName));
                }
                try {
                    return Engine.RenderSystemProvider.DefaultContent.OpenStream(assetName);
                } catch(Exception e) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName), e.InnerException);
                }
            }

            return resource.OpenStream();
        }

        /// <summary>
        /// Locates the specified asset and opens a stream to read it using the resource locator
        /// override. If UseDefaultContent is set to true and the resource cannot be found, an
        /// attempt to locate it from default content will be made.
        /// </summary>
        /// <param name="assetName">Asset name</param>
        /// <param name="locatorOverride">Locator to attempt to locate the asset with</param>
        /// <returns>Stream</returns>
        public Stream OpenStream(String assetName, IResourceLocator locatorOverride) {
            if(String.IsNullOrEmpty(assetName)) {
                throw new NullReferenceException("Asset name is not valid");
            }

            IResource resource = null;

            //First try: Try to locate the resource with the override
            resource =  locatorOverride.LocateResource(assetName);

            //Second try, if resource is not located then try the default content
            if(resource == null) {
                if(!_useDefault) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName));
                }
                try {
                    return Engine.RenderSystemProvider.DefaultContent.OpenStream(assetName);
                } catch(Exception e) {
                    throw new ResourceNotFoundException(String.Format("Resource {0} could not be found", assetName), e.InnerException);
                }
            }

            return resource.OpenStream();
        }

        /// <summary>
        /// Dispose the content manager.
        /// </summary>
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing) {
            if(_isDisposed) {
                if(disposing) {
                    try {
                        foreach(KeyValuePair<String, Object> kv in _loadedAssets) {
                            IDisposable disposable = kv.Value as IDisposable;
                            if(disposable != null) {
                                disposable.Dispose();
                            }
                        }
                    } finally {
                        _loadedAssets.Clear();
                        _loadedAssets = null;
                    }
                }
            }
            _isDisposed = true;
        }
    }
}
